package be.rivendale.mathematics;

import org.apache.commons.lang.Validate;

import static be.rivendale.mathematics.MathematicalUtilities.determinant;
import static be.rivendale.mathematics.Triple.vectorBetweenPoints;

/**
 * Represents a <a href="http://en.wikipedia.org/wiki/Line_(geometry)">line</a> in 3D space.
 * <p>A line is defined in 3D space by two points 'a' and 'b' through with it passes.
 * A line extends infinitely in both directions.</p>
 * <p>An alternative way to define a line is by a point and a vector. This is significant because it allows the line to be
 * formulated in it's parametric form <code>p = p0 + a*t</code> where 'p0' is the given point on the line and 'a' is
 * the vector. The equation is paremetric in 't' which means any 't' will yield a new point 'p' which lies somewhere on
 * this line. (see also {@link #pointOnLine(double)} for more on this subject.</p>
 */
public class Line {
    /**
     * One of the two points that define this line.
     */
    private Point a;

    /**
     * One of the two points that define this line.
     */
    private Point b;

    /**
     * Creates a new line, by passing the two points that define it.
     * @param a One of the two points that define this line.
     * @param b One of the two points that define this line.
     */
    public Line(Point a, Point b) {
        validatePoints(a, b);
        this.a = a;
        this.b = b;
    }

    /**
     * Checks if the specified points are valid to define a line by.
     * This means:<ul><li>They are not equal</li><li>They are not null</li></ul>
     * @param a Point a that defines this line, and needs to be validated.
     * @param b Point b that defines this line, and needs to be validated.
     */
    private void validatePoints(Point a, Point b) {
		String errorMessage = "A line can only be defined in 3D space by two points that are not null.";
		Validate.notNull(a, errorMessage);
		Validate.notNull(b, errorMessage);
		Validate.isTrue(!a.equals(b), "Unable to define a line by two points, when the points are equal.");
    }

    /**
     * Returns one of the points that define this line.
     * @return The first point (a) that defines this line.
     */
    public Point getA() {
        return a;
    }

    /**
     * Returns one of the points that define this line.
     * @return The second point (b) that defines this line.
     */
    public Point getB() {
        return b;
    }

    /**
     * Calculates a new point that lies somewhere on this line.
     * <p>This is done by using parametric equation of the line: <code>p = p0 + [<strong>t</strong> * v] </code> where
     * 'p0' is any given point on the line (in our implementation we choose {@link #getA() a}, 'v' is the line's vector
     * (can be obtained by subtracting {@link #getA() a} from {@link #getB() b}, and 't' is the parametric value of the equation.
     * This means that choosing any value for 't' will yield a new point that is somewhere on this line.</p>
     * <p>There are a number of characteristics for the 't' parameters:<ul><li>Choosing a value of 0 will yield a point
     * equal to {@link #getA() a}</li><li>Choosing a value less the 0 will yield a point on the line that lies before
     * {@link #getA() a}</li><li>Choosing a value of 1 will yield a point equal to {@link #getB() b}</li>
     * Choosing a point between 0 and 1 will yield a point somewhere between {@link #getA() a} and {@link #getB() b}</li>
     * <li>Choosing a point greater then 1 will yield a point after {@link #getB() b}</li></ul> The parameter 't' can thus
     * be used to detemine where on the line the new point lies.</p>
     * @param t The parametric value of the line equation. Choosing any value for this is valid, and will determine where
     * the spawned point will lie on the line.
     * @return A new point that lies somewhere on this line.
     */
    public Point pointOnLine(double t) {
        return a.add(direction().multiply(t).asPoint());
    }

    /**
     * Calculates the <a href="http://en.wikipedia.org/wiki/Slope">slope</a> of this line.
     * <p>In 3D space, a slope can be represented as a <a href="http://en.wikipedia.org/wiki/Direction_vector">
     * direction vector</a></p>
     * <p>In reality a line has two direction vectors, which are eachother's inverse. This funtion only returns one
     * of them (which is the vector from point {@link #getA() a} to point {@link #getB() b}. The other one can
     * be calculated manually by calling {@link Vector#invert()} on the returned vector.</p>
     * @return One of the two slopes or direction vectors of this line. Namely the vector from point a to point b.
     */
    public Vector direction() {
        return vectorBetweenPoints(a, b);
    }

	/**
	 * Calculates the (shortest) distance between the two lines.
	 * <p>The shortest distance between two lines (or just "the distance between two lines" which is the same) is
	 * defined as being the line that is perpendicular to both and passes through both.</p>
	 * <p>If both lines are parallel, the distance is retrieved from the length between any two points</p>
	 * @param line The second line to get the distance for from this line.
	 * @return The distance (in units as defined by the coordinate system) between the two lines.
	 */
	public double distance(Line line) {
		// Validate input contract
		Validate.notNull(line, "To calculate the distance between lines, the second line is required");

		// First calculate some values that can be reused
		Vector vectorBetweenLines = vectorBetweenPoints(this.getA(), line.getA());
		Vector perpendicularVector = this.direction().crossProduct(line.direction());
		double squareLengthOfPerpendicularVector = Math.pow(perpendicularVector.length(), 2);

		// If the (square) length of the cross product between the two line direction vectors is zero
		// then the two lines are parallel
		if(MathematicalUtilities.equals(squareLengthOfPerpendicularVector, 0)) {
			return this.distance(line.getA());
		}

		// All other cases, are normal (skew lines, or intersecting lines)
		// We calculate the parametric value (t) of both lines to determine their closest point of approach.
		double tOnThisLine = determinant(
			(Triple)vectorBetweenLines,
			(Triple) line.direction(),
			(Triple)perpendicularVector
		) / squareLengthOfPerpendicularVector;

		double tOnSpecifiedLine = determinant(
				(Triple)vectorBetweenPoints(this.getA(), line.getA()),
				(Triple) this.direction(),
				(Triple)perpendicularVector
		) / squareLengthOfPerpendicularVector;

		// Finally, we determine the points for the parametric values (t), and measure the distance between them
		// If the lines are intersecting, then the distance will naturally be zero (without exception in the algorithm).
		return distanceBetweenLinesBasedOnParametricValue(this, line, tOnThisLine, tOnSpecifiedLine);
	}

	/**
	 * Calculates the distance between the specified two lines, with the parametric values (t in the line equation
	 * p = p0 + v*t) of their nearest point of approach already calculated.
	 * @param a The first line to calculate the distance for.
	 * @param b The second line to calculate the distance for.
	 * @param tForA The parametric value of the first line's equation where the two lines are nearest
	 * @param tForB The parametric value of the second line's equation where the two lines are nearest
	 * @return The distance between the two lines where at their closest point.
	 */
	private double distanceBetweenLinesBasedOnParametricValue(Line a, Line b, double tForA, double tForB) {
		return vectorBetweenPoints(
			a.pointOnLine(tForA),
			b.pointOnLine(tForB)
		).length();
	}

	public double distance(Point point) {
		Validate.notNull(point, "Distance between a line and a point requires a point");

		Vector vectorBetweenLineAndPoint = vectorBetweenPoints(this.getA(), point);
		Vector projectionVector = vectorBetweenLineAndPoint.project(this.direction().normalize());
		Point projectionPoint = this.getA().add(projectionVector.asPoint());
		return vectorBetweenPoints(projectionPoint, point).length();
	}

}
